package main

import (
	"bufio"
	"bytes"
	_ "embed"
	"fmt"
	"github.com/saintfish/chardet"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
	"io"
	"net/http"
	"os/exec"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

// 打包静态文件
//go:embed static/index.html
var indexFile []byte

//go:embed static/favicon.ico
var icoFile []byte

// 定义一个5长度的缓存用于存放不同进程对应的chan
var lock sync.Mutex
var cacheSize = 5
var chineseDecoder = simplifiedchinese.GBK.NewDecoder()
var consoleChanMapQueue = make([]string, cacheSize+1)
var consoleChanMap = make(map[string]chan string, cacheSize)

func staticPageHandler(w http.ResponseWriter, r *http.Request) {
	http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(indexFile))
}

func staticIcoHandler(w http.ResponseWriter, r *http.Request) {
	http.ServeContent(w, r, "favicon.ico", time.Now(), bytes.NewReader(icoFile))
}

// main 注册页面和端口
func main() {
	http.HandleFunc("/", staticPageHandler)
	http.HandleFunc("/favicon.ico", staticIcoHandler)
	http.HandleFunc("/exec/", execCmd)
	http.HandleFunc("/console/", consoleCmd)
	_ = http.ListenAndServe(":5344", nil)
}

// execCmd 从url中读取命令并写入chan中 返回给页面当前chan的key
func execCmd(w http.ResponseWriter, r *http.Request) {
	if r == nil {
		http.Error(w, "Request is nil", http.StatusInternalServerError)
		return
	}
	urlPath := r.URL.Path
	urlSplit := strings.Split(urlPath, "/")
	if len(urlSplit) < 3 {
		http.Error(w, "Request cmd can not be nil", http.StatusInternalServerError)
		return
	}

	thisCmdKey := strconv.FormatInt(time.Now().UnixMilli(), 10)

	go func(cmdKey string) {
		defer setCacheChan(cmdKey, "end")

		name, cmdHead := getCmdHead()
		cmd := exec.Command(name, cmdHead, strings.Join(urlSplit[2:], " "))
		output, err := cmd.StdoutPipe()
		if err != nil {
			setCacheChan(cmdKey, fmt.Sprintf("cmd StdoutPipe error: %v", err))
			return
		}
		err = cmd.Start()
		if err != nil {
			setCacheChan(cmdKey, fmt.Sprintf("cmd Start error: %v", err))
			return
		}

		reader := bufio.NewReader(output)
		for {
			line, _, err := reader.ReadLine()
			if err != nil {
				if err == io.EOF { // 如果是文件结束（EOF），退出循环
					break
				}
				setCacheChan(cmdKey, fmt.Sprintf("cmd ReadLine error: %v", err))
				return
			}
			utf8Line := detectAndConvertToUTF8(line)
			setCacheChan(cmdKey, utf8Line)
		}
	}(thisCmdKey)

	httpOK(w, thisCmdKey)
	return
}

// consoleCmd 将控制台结果从chan取出并写入返回值
func consoleCmd(w http.ResponseWriter, r *http.Request) {
	if r == nil {
		http.Error(w, "Request is nil", http.StatusInternalServerError)
		return
	}
	urlPath := r.URL.Path
	urlSplit := strings.Split(urlPath, "/")
	if len(urlSplit) < 3 {
		http.Error(w, "Request cmd can not be nil", http.StatusInternalServerError)
		return
	}

	if consoleLine, isContinue := getCacheChanLine(urlSplit[2]); isContinue {
		httpOK(w, consoleLine)
		return
	}

	http.Error(w, "", http.StatusNoContent)
	return
}

// getCmdHead 根据当前操作系统给出命令行前缀
func getCmdHead() (string, string) {
	if runtime.GOOS == "windows" {
		return "cmd", "/c"
	}
	return "bash", "-c"
}

func httpOK(w http.ResponseWriter, result string) {
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.WriteHeader(http.StatusOK)
	_, _ = fmt.Fprint(w, result)
}

func setCacheChan(k string, v interface{}) {
	cacheChanQueueRoll(k) <- fmt.Sprintf("%v", v)
}

// getCacheChanLine 从缓存chan中获取控制台输出内容
func getCacheChanLine(k string) (string, bool) {
	consoleChan := consoleChanMap[k]
	if consoleChan == nil {
		return "", false
	}

	consoleVar := <-consoleChan
	if "end" == consoleVar {
		return "", false
	}

	return consoleVar, true
}

// detectAndConvertToUTF8 将控制台内容转码到UTF8
func detectAndConvertToUTF8(data []byte) string {
	// 检测原始编码
	result, err := chardet.NewTextDetector().DetectBest(data)
	if err != nil {
		return ""
	}

	// 根据检测结果创建相应的解码器
	if result.Charset == "UTF-8" {
		return string(data)
	}

	utf8Data, _, err := transform.Bytes(chineseDecoder, data)
	if err != nil {
		return ""
	}
	return string(utf8Data)
}

// cacheChanQueueRoll 使用一个固定长度的切片来实现缓存队列 限制缓存总数大小 原理大致如下
// 0表示当前队列的尾巴 x表示已存在的缓存 通过每次将尾巴右移一位覆盖之前最老的缓存引用
// 真实的缓存在map中 不直接放在队列中是为了对各个访问者做简单的隔离
//
// 0 0 0 0 0 0
// x 0 0 0 0 0
// x x 0 0 0 0
// x x x 0 0 0
// x x x x 0 0
// x x x x x 0
// 0 x x x x x
// x 0 x x x x
// x x 0 x x x
// x x x 0 x x
// x x x x 0 x
// x x x x x 0
func cacheChanQueueRoll(newCacheId string) chan string {
	lock.Lock()
	defer lock.Unlock()

	consoleChan := consoleChanMap[newCacheId]
	if consoleChan != nil {
		return consoleChan
	}

	for idx, cacheIdExists := range consoleChanMapQueue {
		if "" == cacheIdExists {
			nextCacheIdx := (idx + 1) % len(consoleChanMapQueue)
			oldestCacheId := consoleChanMapQueue[nextCacheIdx]
			delete(consoleChanMap, oldestCacheId)
			consoleChanMapQueue[nextCacheIdx] = ""
			consoleChanMapQueue[idx] = newCacheId
			consoleChanMap[newCacheId] = make(chan string, cacheSize)
			break
		}
	}
	return consoleChanMap[newCacheId]
}
